/* ***** BEGIN LICENSE BLOCK *****
 *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is JsWs.
 *
 * The Initial Developer of the Original Code is
 * Chris Winberry.
 * Portions created by the Initial Developer are Copyright (C) 2007
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 * 
 * ***** END LICENSE BLOCK ***** */

/**
 * @class Exception class for DB errors
 * @constructor
 * @base ExBase
 * @param {String} msg Description of exception
 * @param {Number} code (optional) Error code
 * @param {Exception} ex (optional) An earlier exception to wrap
 * @param {Arguments} args (optional) Arguments array of a functions
 * @see DbConnection
 * @see DbQuery
 */
function ExDbError (msg, code, ex, args) { ExDbError.baseConstructor.call(this, msg, code, ex, args); }
SubClass(ExBase, ExDbError);


/**
 * @class Represents a column in a DB table
 * @constructor
 * @param {Hash/Record} row Hash of a row from a record set
 * @see DbTable
 */
function DbColumn (row) {
	/**
	 * Name of the column
	 * @type String
	 */
	this.name = row["name"];
	/**
	 * Columns data type
	 * @type String
	 */
	this.type = row["type"].toLowerCase();
	/**
	 * ID of the column
	 * @type String
	 */
	this.id = row["cid"];
	/**
	 * Column's default value
	 * @type String
	 */
	this.defVal = row["dflt_value"];
	/**
	 * Flag indicating whether the column is a primary key
	 * @type Boolean
	 */
	this.pk = (row["pk"] == 1);
	/**
	 * Flag indicating if the column allows null values
	 * @type Boolean
	 */
	this.allowNull = (row["notnull"] == 1);
}


/**
 * @class Looks up and represents metadata for a DB table
 * @constructor
 * @param {String} name Name of the table
 * @param {DbConnection} db Database connection to schema that contains the table
 * @requires DbColumn
 */
function DbTable (name, db) {
	/**
	 * Name of the table
	 * @type String
	 */
	this.name = name;
	/**
	 * Columns of the table
	 * @type DbColumn[]
	 */
	this.cols = new Array();

	var query = db.prepare("PRAGMA TABLE_INFO('" + this.name + "')");
	var results = query.exec();
	for (var i = 0; i < results.length; i++)
		this.cols[this.cols.length] = new DbColumn(results[i]);

	query.destroy();
}


/**
 * @class Represents a prepared statement ready for execution with or
 *  without parameters. Execution of the query is handled by {@link #exec}.
 * @constructor
 * @param {DbConnection} dbConn Database connection to schema that contains the table
 * @param {String} sql SQL statement that was prepared
 * @see DbConnection#prepare
 */
function DbQuery (dbConn, sql) {
	/** @private */
	this._conn = dbConn;
	/** @private */
	this._sql = sql;
	/** @private */
	this._query = this._conn.createStatement(this._sql);
}


/**
 * Executes the prepared statement with any parameters passed in
 * @param {Mixed} n Variable number of parameters to be used with the statement
 * @return Data returned by the query
 * @type {Hash/Record}[]
 */
DbQuery.prototype.exec = function (/* n arguments */) {
	for (var i = 0; i < arguments.length; i++) {
		if (arguments[i] == null)
			this._query.bindNullParameter(i);
		else if (typeof(arguments[i]) == "string")
			this._query.bindUTF8StringParameter(i, arguments[i]);
		else if (typeof(arguments[i]) == "number")
			if (parseInt(arguments[i]) != parseFloat(arguments[i]))
				this._query.bindDoubleParameter(i, arguments[i]);
			else
				this._query.bindInt32Parameter(i, arguments[i]);
		else
			this._query.bindUTF8StringParameter(i, arguments[i].toString()); // bindBlobParameter ??? 
	}
	
	var result = new Array();
	while (this._query.executeStep()) {
		var row = new Array();
		for(var i = 0; i < this._query.columnCount; i++)
			row[this._query.getColumnName(i)] = this._query.getUTF8String(i);
		result.push(row);
	}

	this._query.reset();

	return(result);
}


/**
 * Runs cleanup on internal objects
 */
DbQuery.prototype.destroy = function () {
	this._conn = null;
	this._sql = null;
	this._query.reset();
	this._query = null;
}


/**
 * @class Responsible for creating and opening SQLite/MozStorage files. DB files
 *  that don't exist are automatically created. This also provides
 *  methods to enumerate DB files, schema tables, and table columns.
 * @constructor
 * @param {String} name Name of the DB schema to open/create
 */
function DbConnection (name) {
	/** @private */
	this.name = name;
	/** @private */
	this.connection = null;
	this.open();
}


/**
 * Opens the DB. If the DB does not exist, it is first created
 * @exception ExDbError
 */
DbConnection.prototype.open = function () {
	var storageService;
	try {
		var file = this.dbFolder(); // ???
		file.append(this.name + "." + this._extension); // ???

		storageService = Cc["@mozilla.org/storage/service;1"]
			.getService(Ci.mozIStorageService);

		this.connection = storageService.openDatabase(file);
	} catch (ex) {
		this.connection = null;
		throw(new ExDbError("Failed to create/open the database", 0, ex, arguments));
	}
}


/**
 * Closes the DB connection
 */
DbConnection.prototype.close = function () {
	if (this.connection == null)
		return;
	this.destroy();
}


/**
 * Builds a list of tables that exist in the schema
 * @return List of tables in the schema
 * @type {@link DbTable}[]
 */
DbConnection.prototype.tables = function () {
	var tables = new Array();
	var query = this.prepare("SELECT * FROM sqlite_master WHERE type = 'table'");
	var results = query.exec();
	for (var i = 0; i < results.length; i++) {
		var row = results[i];
		tables[tables.length] = new DbTable(row["tbl_name"], this);
	}
	query.destroy();
	return(tables);
}


/**
 * Immediately execute a query that returns no data
 * and has no parameters
 * @param {String} sql SQL query to execute
 * @returns Success or failure of the statement execution
 * @type Boolean
 * @exception ExDbError
 */
DbConnection.prototype.execNoResult = function (sql) {
	if (sql == null || sql == '')
		throw(new ExDbError("SQL statement is null or empty", 0, null, arguments));
	try {
		this.connection.executeSimpleSQL(sql);
	} catch (ex) {
		return(false);
	}
	return(true);
}


/**
 * Prepares a SQL statement that returns data and/or has parameters
 * @param {String} sql SQL query to execute
 * @return Prepare DB query ready for execution
 * @type DbQuery
 * @exception ExDbError
 */
DbConnection.prototype.prepare = function (sql) {
	if (sql == null || sql == '')
		throw(new ExDbError("SQL statement is null or empty", 0, null, arguments));
	return(new DbQuery(this.connection, sql));
}


/**
 * Runs cleanup on internal objects
 */
DbConnection.prototype.destroy = function () {
	this.connection = null;
}


/** @private */
DbConnection._extension = JSWS_DB_FILEEXT;
/** @ignore */
DbConnection.prototype._extension = DbConnection._extension;

/**
 * Directory where DB files are stored
 * @return Directory where DB files are stored
 * @type nsIFile
 */
DbConnection.dbFolder = function () {
	var file = Cc["@mozilla.org/file/directory_service;1"]
		.getService(Ci.nsIProperties)
		.get("ProfD", Ci.nsIFile);
	JsWs.tools.appendPath(file, JSWS_ROOT);
	JsWs.tools.appendPath(file, JSWS_DB_ROOT);
	return(file);
}


/** @ignore */
DbConnection.prototype.dbFolder = DbConnection.dbFolder;

/**
 * Builds a list of existing DB schemas (files)
 * @return An array of existing DB schemas
 * @type String[]
 */
DbConnection.schemas = function () {
	var file = DbConnection.dbFolder();
	var dirEntries = file.directoryEntries;
	var reStorage = new RegExp("(.+)\\." + DbConnection._extension + "$", "i");
	var dbs = new Array();
	while (dirEntries.hasMoreElements()) {
		var dirEntry = dirEntries.getNext().QueryInterface(Ci.nsIFile);
		if (!dirEntry.isDirectory()) {
			var found = reStorage.exec(dirEntry.leafName);
			if (found)
				dbs.push(found[1]);
		}
	}
	return(dbs);
}


/** @ignore */
DbConnection.prototype.schemas = DbConnection.schemas;
